#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include <ngx_http_cache_control_filter_module.h>

static char* ngx_http_cache_control(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static char* ngx_http_cache_control_match_type(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static char* ngx_http_cache_control_cache_size(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

static ngx_int_t ngx_http_cache_control_filter_init(ngx_conf_t *cf);
static void* ngx_http_cache_control_filter_create_main_conf(ngx_conf_t *cf);
static void* ngx_http_cache_control_filter_create_loc_conf(ngx_conf_t *cf);
static char* ngx_http_cache_control_filter_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);

static ngx_int_t ngx_http_cache_control_filter_init_process(ngx_cycle_t *c);
static void ngx_http_cache_control_filter_exit_process(ngx_cycle_t *c);

static ngx_command_t ngx_http_cache_control_filter_command[] = {
    { ngx_string("cache_control"),
      NGX_HTTP_LOC_CONF | NGX_CONF_TAKE2,
      ngx_http_cache_control,
      NGX_HTTP_LOC_CONF_OFFSET,
      0,
      NULL},
    { ngx_string("cache_control_match_type"),
      NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1,
      ngx_http_cache_control_match_type,
      NGX_HTTP_LOC_CONF_OFFSET,
      0,
      NULL},
    { ngx_string("cache_control_cache_size"),
      NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1,
      ngx_http_cache_control_cache_size,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_cache_control_filter_source_t, cache_size),
      NULL},
    ngx_null_command
};

static ngx_http_module_t ngx_http_cache_control_filter_ctx = {
    NULL,                          /* preconfiguration */
    ngx_http_cache_control_filter_init,                          /* postconfiguration */

    ngx_http_cache_control_filter_create_main_conf,                          /* create main configuration */
    NULL,                          /* init main configuration */

    NULL,                          /* create server configuration */
    NULL,                          /* merge server configuration */

    ngx_http_cache_control_filter_create_loc_conf,                          /* create location configuration */
    ngx_http_cache_control_filter_merge_loc_conf                           /* merge location configuration */
};

ngx_module_t ngx_http_cache_control_filter_module = {
    NGX_MODULE_V1,
    &ngx_http_cache_control_filter_ctx,
    ngx_http_cache_control_filter_command,
    NGX_HTTP_MODULE,
    NULL,                                  /* init master */
    NULL,                                  /* init module */
    ngx_http_cache_control_filter_init_process,                                  /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    ngx_http_cache_control_filter_exit_process,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};

static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;

static ngx_int_t ngx_http_set_expire(ngx_http_request_t *r, time_t e)
{
    size_t            len;
    time_t            now, expires_time, max_age;
    ngx_uint_t        i;
    ngx_table_elt_t  *expires, *cc, **ccp;

    expires = r->headers_out.expires;

    if (expires == NULL) {

        expires = ngx_list_push(&r->headers_out.headers);
        if (expires == NULL) {
            return NGX_ERROR;
        }

        r->headers_out.expires = expires;

        expires->hash = 1;
        expires->key.len = sizeof("Expires") - 1;
        expires->key.data = (u_char *) "Expires";
    }

    len = sizeof("Mon, 28 Sep 1970 06:00:00 GMT");
    expires->value.len = len - 1;

    ccp = r->headers_out.cache_control.elts;

    if (ccp == NULL) {

        if (ngx_array_init(&r->headers_out.cache_control, r->pool,
                           1, sizeof(ngx_table_elt_t *))
            != NGX_OK)
        {
            return NGX_ERROR;
        }

        ccp = ngx_array_push(&r->headers_out.cache_control);
        if (ccp == NULL) {
            return NGX_ERROR;
        }

        cc = ngx_list_push(&r->headers_out.headers);
        if (cc == NULL) {
            return NGX_ERROR;
        }

        cc->hash = 1;
        cc->key.len = sizeof("Cache-Control") - 1;
        cc->key.data = (u_char *) "Cache-Control";

        *ccp = cc;

    } else {
        for (i = 1; i < r->headers_out.cache_control.nelts; i++) {
            ccp[i]->hash = 0;
        }

        cc = ccp[0];
    }

    expires->value.data = ngx_palloc(r->pool, len);
    if (expires->value.data == NULL) {
        return NGX_ERROR;
    }
    now = ngx_time();
    expires_time = now + e;
    ngx_http_time(expires->value.data, now + e);

    cc->value.data = ngx_palloc(r->pool,
                                sizeof("max-age=") + NGX_TIME_T_LEN + 1);
    if (cc->value.data == NULL) {
        return NGX_ERROR;
    }
    max_age = e;
    cc->value.len = ngx_sprintf(cc->value.data, "max-age=%T", max_age)
        - cc->value.data;

    return NGX_DECLINED;
}

static ngx_int_t
ngx_http_cache_control_header_filter(ngx_http_request_t *r)
{
    ngx_int_t rc;
    ngx_http_cache_control_filter_main_conf_t *mcf;
    ngx_http_cache_control_filter_loc_conf_t *lcf;
    ngx_http_cache_control_filter_source_t *source;
    time_t expire = 0;

    /* only main request is processed */
    if (r != r->main) {
        return ngx_http_next_header_filter(r);
    }

    /* only GET request is processed */
    if (!(r->method &(NGX_HTTP_GET))) {
        return ngx_http_next_header_filter(r);
    }

    lcf = ngx_http_get_module_loc_conf(r, ngx_http_cache_control_filter_module);

    if (lcf->index == -1) {
        return ngx_http_next_header_filter(r);
    }

    mcf = ngx_http_get_module_main_conf(r, ngx_http_cache_control_filter_module);
    if (mcf == NULL) {
        return ngx_http_next_header_filter(r);
    }

    source = mcf->data_sources.elts;

    rc = source[lcf->index].methods.get_expire_pair(r, &(source[lcf->index]), &expire);

    if (rc == NGX_OK) {
        rc = ngx_http_set_expire(r, expire);
    }

    return ngx_http_next_header_filter(r);
}

static char*
ngx_http_cache_control_match_type(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_cache_control_filter_main_conf_t *mcf;
    ngx_http_cache_control_filter_loc_conf_t *lcf = conf;
    ngx_http_cache_control_filter_source_t *source;
    ngx_str_t *value;

    if (lcf->index == -1) {
        return "cache control module must enabled";
    }

    if (cf->args->nelts != 2) {
        return "invalid parameters";
    }

    mcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_cache_control_filter_module);
    if (mcf == NULL) {
        return "main conf not exists, impossible!";
    }

    source = mcf->data_sources.elts;

    value = cf->args->elts;

    if (!ngx_strncmp(value[1].data, "absolute", strlen("absolute"))) {
        source[lcf->index].match_type = NGX_HTTP_CACHE_CONTROL_FILTER_MATCH_TYPE_ABSOLUTE;
    } else if (!ngx_strncmp(value[1].data, "prefix", strlen("prefix"))) {
        source[lcf->index].match_type = NGX_HTTP_CACHE_CONTROL_FILTER_MATCH_TYPE_PREFIX;
    } else {
        return "unsupported match type";
    }

    return NGX_CONF_OK;
}

static char*
ngx_http_cache_control_cache_size(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_cache_control_filter_main_conf_t *mcf;
    ngx_http_cache_control_filter_loc_conf_t *lcf = conf;
    ngx_http_cache_control_filter_source_t *source;
    char *set_slot_rc = NULL;

    if (lcf->index == -1) {
        return "cache control module must enabled";
    }

    if (cf->args->nelts != 2) {
        return "invalid parameters";
    }

    mcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_cache_control_filter_module);
    if (mcf == NULL) {
        return "main conf not exists, impossible!";
    }

    source = mcf->data_sources.elts;

    /* this is tricky, for that I don't want to implement the init_main_conf method */
    source[lcf->index].cache_size = NGX_CONF_UNSET_UINT;

    if((set_slot_rc = ngx_conf_set_num_slot(cf, cmd, &(source[lcf->index]))) != NGX_CONF_OK) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, set_slot_rc);
        return NGX_CONF_ERROR;
    }

    return NGX_CONF_OK;
}


static char*
ngx_http_cache_control(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_cache_control_filter_main_conf_t *mcf;
    ngx_http_cache_control_filter_loc_conf_t *lcf = conf;
    ngx_http_cache_control_filter_source_t *source;
    ngx_http_cache_control_filter_supported_source_t *s_source;
    ngx_str_t *value;

    if (lcf->index != -1) {
        return "is duplicate";
    }

    if (cf->args->nelts != 3) {
        return "invalid parameters";
    }

    mcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_cache_control_filter_module);
    if (mcf == NULL) {
        return "the data sources array should exists first";
    }

    source = ngx_array_push(&(mcf->data_sources));
    if (source == NULL) {
        return "fail to get data source memory";
    }

    value = cf->args->elts;

    for (s_source = supported_sources; s_source->type != NULL; s_source++) {
        if (!ngx_strncmp(value[1].data, s_source->type, strlen(s_source->type))) {
            source->type = s_source->type;
            source->path.data = value[2].data;
            source->path.len = value[2].len;
            source->methods = s_source->methods;
            break;
        }
    }

    if (source->type == NULL) {
        return "not supported data source";
    }

    source->cache_size = DEFAULT_CACHE_SIZE;
    source->match_type = DEFAULT_MATCH_TYPE;

    lcf->index = mcf->data_sources.nelts - 1;

    return NGX_CONF_OK;
}

static ngx_int_t
ngx_http_cache_control_filter_init(ngx_conf_t *cf)
{
    ngx_http_next_header_filter = ngx_http_top_header_filter;
    ngx_http_top_header_filter = ngx_http_cache_control_header_filter;

    return NGX_OK;
}

static void*
ngx_http_cache_control_filter_create_main_conf(ngx_conf_t *cf)
{
    ngx_int_t ret;
    ngx_http_cache_control_filter_main_conf_t *mcf;

    mcf = ngx_palloc(cf->pool, sizeof(ngx_http_cache_control_filter_main_conf_t));
    if (mcf == NULL) {
        return "fail to alloc memory";
    }

    ret = ngx_array_init(&(mcf->data_sources), cf->pool, 1, sizeof(ngx_http_cache_control_filter_source_t));
    if (ret) {
        return "fail to initialize data sources array";
    }

    return mcf;
}

static void*
ngx_http_cache_control_filter_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_cache_control_filter_loc_conf_t *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_cache_control_filter_loc_conf_t));
    if (conf == NULL) {
        return NGX_CONF_ERROR;
    }

    conf->index = -1;
    return conf;
}

static char*
ngx_http_cache_control_filter_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_cache_control_filter_loc_conf_t *prev = parent;
    ngx_http_cache_control_filter_loc_conf_t *lcf = child;

    if (lcf->index == -1) {
        lcf->index = prev->index;
    }

    return NGX_CONF_OK;
}

static ngx_int_t
ngx_http_cache_control_filter_init_process(ngx_cycle_t *c)
{
    ngx_int_t ret = NGX_OK;
    ngx_http_conf_ctx_t *ctx;
    ngx_http_cache_control_filter_main_conf_t *mcf;
    ngx_http_cache_control_filter_source_t *source;
    ngx_uint_t i;

    ctx = (ngx_http_conf_ctx_t *)ngx_get_conf(c->conf_ctx, ngx_http_module);

    mcf = ctx->main_conf[ngx_http_cache_control_filter_module.ctx_index];

    source = mcf->data_sources.elts;
    for(i = 0; i< mcf->data_sources.nelts; i++) {
        ret = source[i].methods.init_process(c, &(source[i]));
        if (ret != NGX_OK) {
            return ret;
        }
    }

    return ret;
}

static void
ngx_http_cache_control_filter_exit_process(ngx_cycle_t *c)
{
    ngx_http_conf_ctx_t *ctx;
    ngx_http_cache_control_filter_main_conf_t *mcf;
    ngx_http_cache_control_filter_source_t *source;
    ngx_uint_t i;

    ctx = (ngx_http_conf_ctx_t *)ngx_get_conf(c->conf_ctx, ngx_http_module);

    mcf = ctx->main_conf[ngx_http_cache_control_filter_module.ctx_index];

    source = mcf->data_sources.elts;
    for(i = 0; i< mcf->data_sources.nelts; i++) {
        source[i].methods.exit_process(c, &(source[i]));
    }

    return;
}
